package ezXpns.storage;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.UUID;

import org.apache.commons.lang3.tuple.Pair;
import org.apache.lucene.document.Document;

import com.fasterxml.uuid.EthernetAddress;
import com.fasterxml.uuid.Generators;
import com.fasterxml.uuid.impl.TimeBasedGenerator;

import ezXpns.analysis.Transform;

/**
 * Key-value storage for any data type with a pluggable storage engine <br />
 * Keys are always of type UUID and are randomly generated <br />
 * Listeners can be added to the storage to implement indexing and trigger
 * 
 * @see ezXpns.storage.StorageEngine
 * @param <T> Value type
 */

//@author A0088827L
public class Storage<T> implements AutoCloseable {
	
	/**
	 * Construct a storage with the given engine
	 * 
	 * @see ezXpns.storage.StorageEngine
	 * @param engine storage engine to use
	 * @throws IOException 
	 */
	public Storage(StorageEngine<T> engine, Transform<Pair<UUID, T>, Document> converter) throws IOException {
		this.engine = engine;
		this.searcher = new Searcher<T>(this, converter);
		for(Entry<UUID, T> entry: engine.getAllEntries()) {
			searcher.index(entry);
		}
	}
	
	/**
	 * Add an object to the storage
	 * @param obj the object to add
	 * @return the generated UUID
	 */
	public UUID add(T obj) {
		UUID id = generateUUID();
		addWithId(id, obj);
		return id;
	}
	
	/**
	 * Add an object to the storage with a predefined id
	 * @param id id of the object
	 * @param obj the object to be added
	 */
	public void addWithId(UUID id, T obj){
		engine.add(id, obj);

		for(StorageListener<T> listener: listeners){
			listener.onAdd(id, obj);			
		}
	}
	
	/**
	 * Get an object by id
	 * @param id object id
	 * @return the object or null if it is not found
	 */
	public T get(UUID id) {
		return engine.get(id);
	}
	
	/**
	 * Remove an object from the storage
	 * @param id id of object to remove
	 * @return the removed object or null if nothing is removed
	 */
	public T remove(UUID id) {
		T obj = engine.remove(id);
		
		if(obj != null) {
			for(StorageListener<T> listener: listeners) {
				listener.onRemove(id, obj);
			}
		}
		
		return obj;
	}
	
	/**
	 * Replace an object
	 * @param id id of object to replace
	 * @param newObj the new object
	 * @return the old object or null if nothing is replaced
	 */
	public T replace(UUID id, T newObj) {
		T oldObj = engine.replace(id, newObj);
		
		if(oldObj != null) {
			for(StorageListener<T> listener: listeners) {
				listener.onReplace(id, oldObj, newObj);
			}	
		}		
		
		return oldObj;
	}
	
	/**
	 * Find all objects which satisfy a predicate  <br />
	 * Remarks: the returned stream can be lazy. There is no guarantee of data integrity if
	 * data is inserted or removed from the storage while the stream is being integrated.
	 * @param predicate the predicate to use for searching
	 * @return a stream of satisfied {id, object} pair
	 */
	public Iterable<Entry<UUID, T>> getAllEntries() {
		return engine.getAllEntries();
	}
	
	/**
	 * Query the storage
	 * 
	 * @param query the query to be used
	 * @return all entries which match the query
	 */
	public Iterable<Entry<UUID, T>> query(Query<T> query) {
		try {
			return searcher.search(query, 100);
		} catch (IOException e) {
			e.printStackTrace();
			return EMPTY_LIST;
		}
	}
	
	/**
	 * Query the storage and get notified about future update	
	 * @param query the query to be used
	 * @param handler a handler which will receive all notifications about relevant data
	 * @return a connection object to unsubscribe when updates are no longer needed
	 * @see LiveQueryConnection
	 */
	public LiveQueryConnection liveQuery(Query<T> query, StorageListener<T> handler) {
		try {
			Iterable<Entry<UUID, T>> initialResult = searcher.search(query, 100);
			for(Entry<UUID, T> entry: initialResult) {
				handler.onAdd(entry.getKey(), entry.getValue());
			}
			StorageListener<T> listener = new StorageEventFilter(query, handler);
			addListener(listener);
			return new LiveQueryConnectionImpl(listener);
		} catch (IOException e) {
			e.printStackTrace();
		}

		return null;
	}
	
	/**
	 * Query the storage and get notified about future update using multiple handlers which share the same connection <br />
	 * Once the connection is disconnected all handlers stop receiving updates.
	 * @param query the query to be used
	 * @param handlers handlers which will receive all notifications about relevant data
	 * @return a connection object to unsubscribe when updates are no longer needed
	 * @see LiveQueryConnection
	 */	
	@SafeVarargs
	public final LiveQueryConnection liveQueryMulti(Query<T> query, StorageListener<T>... handlers) {
		return liveQuery(query, new CompoundStorageListener<T>(handlers));
	}
	
	/**
	 * Add a listener to this storage
	 * @see ezXpns.storage.StorageListener
	 * @param listener the listener to add
	 */
	public void addListener(StorageListener<T> listener) {
		listeners.add(listener);
	}
	
	/**
	 * Remove a listener from this storage
	 * @see ezXpns.storage.StorageListener
	 * @param listener the listener to remove
	 */
	public void removeListener(StorageListener<T> listener) {
		listeners.remove(listener);
	}
	
	/**
	 * Save data to an output stream
	 * @param output the stream to save data to
	 * @throws IOException
	 */
	public void save() throws IOException {
		engine.save();
	}
	
	/**
	 * Close the storage and persist all unsaved changes
	 * No modification can be made after this point
	 */
	@Override
	public void close() throws Exception {
		searcher.close();
		engine.close();
	}
	
	/**
	 * Generate an id for a new object
	 * @return the new UUID
	 */
	protected UUID generateUUID() {
		EthernetAddress nic = EthernetAddress.fromInterface();
		TimeBasedGenerator uuidGenerator = Generators.timeBasedGenerator(nic);
		return uuidGenerator.generate();
	}
	
	private final Iterable<Entry<UUID, T>> EMPTY_LIST = (new HashMap<UUID, T>()).entrySet();

	private class StorageEventFilter implements StorageListener<T>{
		public StorageEventFilter(Query<T> query, StorageListener<T> handler) {
			this.query = query;
			this.handler = handler;
		}

		@Override
		public void onAdd(UUID id, T obj) {
			if(query.apply(obj))
				handler.onAdd(id, obj);
		}

		@Override
		public void onRemove(UUID id, T obj) {
			if(query.apply(obj)) {
				handler.onRemove(id, obj);
			}
		}

		@Override
		public void onReplace(UUID id, T oldObj, T newObj) {
			if(query.apply(oldObj)) {
				if(query.apply(newObj)) {//edit into an object that match the query
					handler.onReplace(id, oldObj, newObj);
				}
				else {//edit into an object that doesn't match the query
					//consider it removed
					handler.onRemove(id, oldObj);
				}
			}
			else {//edit into something that match the query
				if(query.apply(newObj)) {
					handler.onAdd(id, newObj);
				}
			}
		}

		private Query<T> query;
		private StorageListener<T> handler;
	}

	private class LiveQueryConnectionImpl implements LiveQueryConnection {
		public LiveQueryConnectionImpl(StorageListener<T> listener) {
			this.listener = listener;
		}
		
		@Override
		public void disconnect() {
			removeListener(listener);
		}
		
		private StorageListener<T> listener;
	}

	private HashSet<StorageListener<T>> listeners = new HashSet<StorageListener<T>>();
	private StorageEngine<T> engine;
	private Searcher<T> searcher;
}